<?php
/**
 * Apricot Kernel file
 * <pre>
 *     _               _           _
 *    / \   _ __  _ __(_) ___ ___ | |_
 *   / _ \ | '_ \| '__| |/ __/ _ \| __|
 *  / ___ \| |_) | |  | | (_| (_) | |_
 * /_/   \_\ .__/|_|  |_|\___\___/ \__|
 *         |_|
 * </pre>
 *
 * @package Apricot
 * @version $Id$
 */

/**
 * Apricot_Kernel
 *
 * @package Apricot
 * @author Jansen Price <jansen.price@gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 * @version ApricotVersion: 1.1
 */
class Apricot_Kernel
{
    /**
     * Config object
     * 
     * @var Apricot_Config
     */
    protected $_config = null;

    /**
     * The request object (controller, action, params)
     *
     * @var Apricot_Request
     */
    protected $_request = null;

    /**
     * The model cache
     *
     * @var array
     */
    protected $_models = array();

    /**
     * Whether application is a terminal
     *
     * @var bool
     */
    protected $_isTerm = false;

    /**
     * Storage for the Terminal object
     *
     * @var Qi_Console_Terminal
     */
    protected $_terminal = null;

    /**
     * Storage for the session object
     * 
     * @var mixed
     */
    protected $_session = null;

    /**
     * Modules are addins or extensions
     * 
     * @var array
     */
    protected $_modules = array();

    /**
     * Constructor
     *
     * @param Apricot_Config $config Configuration object
     * @return void
     */
    public function __construct(Apricot_Config $config = null)
    {
        $this->setConfig($config);
        if (null != $config) {
            $this->importModules($config->modules);
        }
    }

    /**
     * Set config object
     * 
     * @param object $config Config object
     * @return void
     */
    public function setConfig(Apricot_Config $config = null)
    {
        if (null == $config) {
            $config = self::makeConfig();
        }
        $this->_config = $config;
        $this->init();
    }

    /**
     * Get the config setting for a given key
     *
     * @param string $setting The desired key name
     * @return mixed
     */
    public function getConfig($setting = null)
    {
        if (null === $setting) {
            return $this->_config;
        } else {
            return $this->_config->get($setting);
        }
    }

    /**
     * Initialize (after config is loaded)
     *
     * @return void
     */
    public function init()
    {
        // Set up terminal handling
        if ($this->getConfig('output') == 'term'
            || $this->getConfig('output') == 'console'
        ) {
            $this->_isTerm   = true;
            $this->_terminal = self::makeTerminal();
        } else {
            $this->_isTerm   = false;
            $this->_terminal = null;
        }

        // Set the error_reporting details
        $reportingLevel = $this->getConfig('reporting_level');
        if (null == $reportingLevel) {
            $reportingLevel = E_ALL | E_STRICT;
        }
        error_reporting($reportingLevel);
        if ($this->getConfig('env') == 'production') {
            // force no display_errors in production
            $this->getConfig()->set('display_errors', '0');
        }
        ini_set('display_errors', $this->getConfig('display_errors'));
    }

    /**
     * Make default config object
     * 
     * @return object Apricot_Config
     */
    public static function makeConfig()
    {
        return new Apricot_Config();
    }

    /**
     * Make default terminal object
     * 
     * @return object Qi_Console_Terminal
     */
    public static function makeTerminal()
    {
        include_once 'Qi/Console/Terminal.php';
        return new Qi_Console_Terminal();
    }

    /**
     * Make cache object
     * 
     * @return object
     */
    public function makeCache()
    {
        include_once 'lib/Apricot/Cache.php';
        return new Apricot_Cache($this);
    }

    /**
     * Make View object
     * 
     * @param mixed $options Optional options passed to constructor
     * @return object
     */
    public function makeView($options = null)
    {
        if ($this->getConfig('view_class')) {
            $viewClassName = $this->getConfig('view_class');
        } else {
            if (!$this->isTerminal()) {
                $viewClassName = 'ApplicationView';
            } else {
                $viewClassName = 'CliView';
            }
        }

        $this->_loadClass($viewClassName);

        if (isset($options['engine'])) {
            $engine = $options['engine'];
        } else {
            $engine = $this->makeDefaultViewEngine(null, $options);
        }

        $view = new $viewClassName($this, $engine, $options);
        return $view;
    }

    /**
     * Make default view engine
     * 
     * @param string $engine The view engine object or class name
     * @param array $options Options to pass to view engine constructor
     * @return object
     */
    public function makeDefaultViewEngine($engine = null, $options = array())
    {
        if (null === $engine) {
            $engine = $this->getConfig('default_view_engine');
            if (trim($engine) == '') {
                $engine = 'Apricot_View_Htmldoc';
            }
        }

        if (!class_exists($engine)) {
            include_once 'lib' . DIRECTORY_SEPARATOR
                . str_replace('_', DIRECTORY_SEPARATOR, $engine)
                . '.php';
        }

        return new $engine($options);
    }

    /**
     * Make a module
     * 
     * @param string $class The class name
     * @param array $options Options to pass to constructor
     * @return object
     */
    public function makeModule($class, $options = array())
    {
        return new $class($this, $options);
    }

    /**
     * Make the generic module
     * 
     * @return Apricot_Kernel_GenericModule
     */
    public function makeGenericModule()
    {
        include_once 'Apricot/Module/Generic.php';
        return new Apricot_Module_Generic($this);
    }

    /**
     * Make sesspool object
     * 
     * @param string $name Session namespace
     * @return object
     */
    public function makeSesspool($name)
    {
        include_once 'Qi/Sesspool.php';
        return new Qi_Sesspool($name);
    }

    /**
     * Set the session handler object
     *
     * @param mixed $session The session handler object
     * @return void
     */
    public function setSession($session)
    {
        $this->_session = $session;
    }

    /**
     * Get session
     * 
     * @return object Session object
     */
    public function getSession()
    {
        if (null == $this->_session) {
            // Set the sesspool object (if not already set)
            $namespace = $this->getConfig('session_namespace');
            $this->setSession($this->makeSesspool($namespace));
        }

        return $this->_session;
    }

    /**
     * Dispatch request to a controller
     *
     * @param object $request The request object
     * @return void
     */
    public function dispatch(Apricot_Request $request)
    {
        $this->setRequest($request);

        $controllerClassName =
            $this->loadController($request->controller);

        $controller = new $controllerClassName($this, $request);
        return $controller->execute();
    }

    /**
     * Set request object
     *
     * TODO: enforce interface for request object?
     *
     * @param object $request Request object
     * @return void
     */
    public function setRequest($request)
    {
        $this->_request = $request;
    }

    /**
     * Log message
     * 
     * @param mixed $input Message to log
     * @param mixed $logFile Log file
     * @return bool Whether the message was logged
     */
    public function log($input, $logFile = null)
    {
        $logger = $this->getModule('logger');
        return $this->getModule('logger')->log($input, $logFile);
    }

    /**
     * Get the Apricot request
     *
     * @param bool $refresh Whether to force a reparse on the request
     * @return object Request
     */
    public function getRequest($refresh = false)
    {
        if (!isset($this->_request)) {
            // If it hasn't been set yet, return a new blank request
            return new Apricot_Request();
        }

        if ($refresh == true) {
            $this->_request->refresh();
        }

        return $this->_request;
    }

    /**
     * Return whether this request is from a terminal
     *
     * @return bool
     */
    public function isTerminal()
    {
        return $this->_isTerm;
    }

    /**
     * Get the Terminal object
     *
     * @return object Qi_Console_Terminal The terminal object
     */
    public function getTerminal()
    {
        return $this->_terminal;
    }

    /**
     * Get an application model
     *
     * @param string $model The name of the model to get
     * @param array $dbCfg Array of alternate db config settings
     * @param bool $cache Use the cache
     * @return object Apricot_Model The model
     */
    public function getModel($model, $dbCfg = null, $cache = true)
    {
        if (!$cache 
            || ($cache && !isset($this->_models[$model]))
        ) {
            $modelClassName = ucfirst($model) . "Model";

            // Instantiate the class
            // TODO: Support multiple dbs per app
            if (null === $dbCfg) {
                if ($this->getConfig('db')) {
                    $dbCfg = $this->getConfig('db');
                } else {
                    $dbCfg = array();
                }
            }

            $this->_loadClass($modelClassName);

            try {
                if ($cache) {
                    $this->_models[$model] = new $modelClassName($this, $dbCfg);
                } else {
                    $object = new $modelClassName($this, $dbCfg);
                }
            } catch (Exception $e) {
                throw new Apricot_KernelException(
                    "Cannot load model '$model'. "
                    . "Error: " . $e->getCode() . ' '
                    . $e->getMessage(),
                    Apricot_KernelException::ERROR_LOAD_MODEL
                );
            }
        }

        if ($cache) {
            return $this->_models[$model];
        } else {
            return $object;
        }
    }

    /**
     * load_controller
     *
     * @param mixed $name The name of the controller
     * @return void
     */
    public function loadController($name)
    {
        $controllerClassName = ucfirst($name) . "Controller";

        if (class_exists($controllerClassName)) {
            return $controllerClassName;
        }

        $controllerFilePath = $this->getConfig('app_root')
            . DIRECTORY_SEPARATOR
            . $this->getConfig('controllers_dir')
            . $controllerClassName . '.php';

        if (!file_exists($controllerFilePath)) {
            // Controller does not exist
            throw new Apricot_KernelException(
                "Cannot load controller '$name'.",
                Apricot_KernelException::ERROR_LOAD_CONTROLLER
            );
        }

        include_once $controllerFilePath;
        return $controllerClassName;
    }

    /**
     * Attempt to load a class
     *
     * @param mixed $classname The name of the class to load
     * @return void
     */
    private function _loadClass($classname)
    {
        if (class_exists($classname)) {
            return true;
        }

        $classname_section = strtolower(substr($classname, -4));

        switch ($classname_section) {
        case "view":
            $path = $this->getConfig('views_dir');
            break;
        case "mode": // model
        case "odel":
            $path = $this->getConfig('models_dir');
            break;
        default:
            break;
        }

        $file = $this->getConfig('app_root') . DIRECTORY_SEPARATOR
            . $path . str_replace("_", ".", $classname) . ".php";

        if (!file_exists($file)) {
            throw new Apricot_KernelException(
                "Cannot load class. File '$file' doesn't exist.",
                Apricot_KernelException::ERROR_CLASS_NOT_FOUND
            );
        }

        include_once $file;
    }

    /**
     * Import modules
     * 
     * @param array $modules List of modules
     * @return void
     */
    public function importModules($modules)
    {
        if (empty($modules)) {
            return false;
        }

        foreach ($modules as $name => $module) {
            if (isset($module->path)) {
                try {
                    include_once $module->path;
                } catch (Exception $e) {
                    throw new Apricot_KernelException(
                        "Cannot load module '" . $module->path . "'. "
                            . $e->getMessage()
                    );
                }
            }

            if (!class_exists($module->class)) {
                continue;
            }

            $class  = $module->class;
            $object = $this->makeModule($class, $module);

            $this->loadModule($object, $name);
        }
    }

    /**
     * Load module into Kernel memory
     * 
     * @param Apricot_Module_Interface $module The module object
     * @param mixed $name Name of the module key
     * @return void
     */
    public function loadModule(Apricot_Module_Interface $module,
        $name = null)
    {
        if (null === $name) {
            $name = get_class($module);
        }
        $module->register();

        $this->_modules[$name] = $module;
    }

    /**
     * Get a module by name
     * 
     * @param string $name Name of module key
     * @return object|null
     */
    public function getModule($name)
    {
        if (isset($this->_modules[$name])) {
            return $this->_modules[$name];
        }

        // If no module was found, return the generic one
        return $this->makeGenericModule();
    }

    /**
     * Magic call is passed to any modules (if exist)
     * 
     * @param string $method Method invoked
     * @param array $args Arguments
     * @return mixed
     */
    public function __call($method, $args)
    {
        $module = $this->getModule($method);

        if (count($args) == 0) {
            return $module;
        }

        return call_user_func_array(array($module, 'direct'), $args);
    }

    /**
     * Assign view to modules
     * 
     * @param Apricot_View $view View
     * @return void
     */
    public function assignViewToModules($view)
    {
        foreach ($this->_modules as $module) {
            if ($module->getType() == Apricot_Module_Abstract::TYPE_VIEW) {
                $module->view = $view;
            }
        }
    }
}

/**
 * Apricot_KernelException
 *
 * @uses Exception
 * @package Apricot
 * @author Jansen Price <jansen.price@gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 * @version ApricotVersion: 1.1
 */
class Apricot_KernelException extends Exception
{
    const ERROR_LOAD                  = 1;
    const ERROR_CONTROLLER            = 2;
    const ERROR_VIEW                  = 4;
    const ERROR_INDEX                 = 8;
    const ERROR_MASTER                = 16;
    const ERROR_MODEL                 = 32;
    const ERROR_LOAD_CONTROLLER       = 3;
    const ERROR_LOAD_VIEW             = 5;
    const ERROR_LOAD_CONTROLLER_INDEX = 11;
    const ERROR_LOAD_VIEW_INDEX       = 13;
    const ERROR_LOAD_VIEW_MASTER      = 21;
    const ERROR_LOAD_MODEL            = 33;
    const ERROR_CLASS_NOT_FOUND       = 64;
}
